/*
 * Famon (http://code.google.com/p/famon/)
 * Copyright (c) 2010 Carl Crous.
 *
 * This file is part of Famon.
 *
 * Famon is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Famon is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Famon.  If not, see <http://www.gnu.org/licenses/>.
 */

package za.org.foobar.famon.frontend.client.framework;

import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.dom.client.Document;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Widget;
import org.gwtportlets.portlet.client.util.GenUtil;
import za.org.foobar.famon.frontend.client.util.ClientUtil;

import java.util.*;

/**
 *
 */
public class EasyGrid<T> extends Widget
        implements HasClickHandlers, HasAllMouseHandlers, Comparator<Integer>, HasChangeHandlers {


    protected List<T> data = new ArrayList<T>();
    // data[i] = row[j]
    // dataToRow[i] = j
    // rowToData[j] = i
    protected List<Integer> dataToRow = new ArrayList<Integer>();
    protected List<Integer> rowToData = new ArrayList<Integer>();

    protected StringBuffer html;
    protected Renderer<T> renderer;
    protected int sortCol = -1;
    protected boolean sortDesc;

    protected Element divElem;
    protected Element tableElem;
    protected Element bodyElem;

    protected String tableStyleName;
    protected String borderWidth;
    protected String cellPadding;
    protected String cellSpacing;
    protected String width;
    protected String sortAscImage = "img/asc.gif";
    protected String sortDescImage = "img/desc.gif";
    protected String styleName = "EasyGrid";
    protected String headerStyleName = "EasyGridHeader";
    protected String selectedStyleName = "selected";

    private Event lastEvent;
    private SortState sortState;
    private int selectedIndex = -1;
    private int selectedColumn = -1;
    private boolean showSelection = true;

    public EasyGrid() {
        setElement(divElem = DOM.createDiv());
        tableElem = DOM.createTable();
        DOM.appendChild(divElem, tableElem);
        sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS);
        setStyleName(styleName);
        setCellPadding(4);
        setCellSpacing(0);
        setBorderWidth(1);
    }

    public EasyGrid(Renderer<T> renderer) {
        this();
        this.renderer = renderer;
    }

    public Renderer getRenderer() {
        return renderer;
    }

    public void setRenderer(Renderer<T> renderer) {
        this.renderer = renderer;
    }

    public void setStyleName(String style) {
        tableStyleName = style;
    }

    public String getStyleName() {
        return tableStyleName;
    }

    public void setBorderWidth(int width) {
        borderWidth = Integer.toString(width);
    }

    public void setCellPadding(int padding) {
        cellPadding = Integer.toString(padding);
    }

    public void setCellSpacing(int spacing) {
        cellSpacing = Integer.toString(spacing);
    }

    public String getWidth() {
        return width;
    }

    public void setWidth(String width) {
        this.width = width;
    }

    // Row 0 = data 0 (after header)
    public T getData(int row) {
        return data.get(rowToData.get(row));
    }

    public void setData(int row, T value) {
        data.set(rowToData.get(row), value);
    }

    /**
     * Add data to the grid.
     */
    public void addAllData(T[] a) {
        if (a != null) {
            for (T o : a) {
                addData(o);
            }
        }
    }

    /**
     * Add data to the grid.
     */
    public void addAllData(Collection<T> c) {
        for (T o : c) {
            addData(o);
        }
    }

    /**
     * Add data to the grid.
     */
    public void addData(T rowData) {
        data.add(rowData);
        rowToData.add(data.size()-1);
        dataToRow.add(rowToData.size()-1);
    }

    /**
     * Remove data from the grid.
     */
    public void removeData(int row) {
        data.remove(row);
        rowToData.remove(row);
        dataToRow.remove(new Integer(row));
    }

    /**
     * Remove all data from the grid.
     */
    public void clearData() {
        setSelectedIndex(-1);
        data.clear();
        rowToData.clear();
        dataToRow.clear();
    }

    public int getRowCount() {
        return data.size();
    }

    /**
     * Repaint our table. This rebuilds the HTML for the table from scratch
     * and sets the innerHTML for our DIV.
     */
    public void repaint() {
        html = new StringBuffer();
        startTable(html);
        for (int i = 0; i < data.size(); i++) {
            renderRow(html, i);
        }
        renderAfterLastRow(html);
        html.append("</tbody></table>");
        DOM.setInnerHTML(divElem, html.toString());
        tableElem = DOM.getFirstChild(divElem);
        bodyElem = DOM.getFirstChild(tableElem);
    }

    protected void startTable(StringBuffer html) {
        html.append("<table border=\"");
        html.append(borderWidth);
        html.append("\" cellspacing=\"");
        html.append(cellSpacing);
        html.append("\" cellpadding=\"");
        html.append(cellPadding);
        html.append("\" class=\"");
        html.append(tableStyleName);
        if (width != null) {
            html.append("\" width=\"");
            html.append(width);
        }
        html.append("\"><tbody>");
        renderer.renderHeader(this, html);
    }

    protected void renderRow(StringBuffer html, int row) {
        renderer.renderRow(this, html, row, data.get(rowToData.get(row)));
    }

    @SuppressWarnings({"UnusedDeclaration"})
    protected void renderAfterLastRow(StringBuffer html) {
    }

    public void onBrowserEvent(Event event) {
        lastEvent = event;
        switch (DOM.eventGetType(event)) {
            case Event.ONCLICK:
                fireCellClicked(event);
                break;
            case Event.ONMOUSEDOWN:
            case Event.ONMOUSEUP:
            case Event.ONMOUSEMOVE:
            case Event.ONMOUSEOVER:
            case Event.ONMOUSEOUT:
                DomEvent.fireNativeEvent(event, this);
                break;
        }
    }

    /**
     * Get the last event object we have received.
     */
    public Event getLastEvent() {
        return lastEvent;
    }

    protected void fireCellClicked(Event event) {
        String token = GenUtil.getTargetHistoryToken(event);
        if (token == null) {
            DomEvent.fireNativeEvent(event, this);
        }
        Cell cell = getCellForEvent(event);
        if (cell == null) {
            return;
        }
        if (cell.row == 0) {
            Column col = renderer.columns[cell.col];
            if (col.isSortable()) {
                boolean desc = cell.col == sortCol && !sortDesc;
                sort(cell.col, desc);
                if (sortState != null) {
                    sortState.add(cell.col, desc);
                }
            }
        } else {
            setSelectedIndex(cell.row, cell.col);
        }
    }

    public Cell getCellForEvent(Event event) {
        Element td = getEventTargetCell(event);
        if (td == null) {
            return null;
        }
        Element tr = DOM.getParent(td);
        Element body = DOM.getParent(tr);
        Cell cell = new Cell();
        cell.row = DOM.getChildIndex(body, tr);
        cell.col = DOM.getChildIndex(tr, td);
        return cell;
    }

    /**
     * Sort by col.
     */
    public void sort(int col, boolean desc) {
        sortCol = col;
        this.sortDesc = desc;
        Object so = getSelectedObject();
        Object[] a = rowToData.toArray();
        GenUtil.mergeSort(a, this);
        for (int i = 0; i < rowToData.size(); i++) {
            Integer val = (Integer)a[i];
            rowToData.set(i, val);
            dataToRow.set(val, i);
        }
        selectedIndex = -1;
        repaint();
        if (so != null && showSelection) {
            for (int i = 1; i < data.size(); i++) {
                if (data.get(i) == so) {
                    setSelectedIndex(i, selectedColumn);
                    break;
                }
            }
        }
    }

    public void filter() {
        dataToRow.clear();
        rowToData.clear();
        int j = 0;
        for (int i = 0; i < data.size(); i++) {
            T d = data.get(i);
            if (renderer.isVisible(this, d)) {
                rowToData.add(j);
                j++;
                dataToRow.add(i);
            } else {
                dataToRow.add(-1);
            }
        }
    }

    /**
     * Determines the TD associated with the specified event or null if none.
     * Its parent (<code>DOM.getParent(td)</code>) is a TR.
     */
    public Element getEventTargetCell(Event event) {
        Element td;
        try {
            td = DOM.eventGetTarget(event);
        } catch (JavaScriptException e) {
            // sometimes getting the target throws an exception for some reason
            return null;
        }
        for (; td != null; td = DOM.getParent(td)) {
            // If it's a TD, it might be the one we're looking for.
            if (isTDorTH(td)) {
                // Make sure it's directly a part of this table before returning it.
                Element tr = DOM.getParent(td);
                Element body = DOM.getParent(tr);
                if (body == bodyElem) {
                    return td;
                }
            }
            // If we run into this table's body, we're out of options.
            if (td == bodyElem) {
                return null;
            }
        }
        return null;
    }

    /**
     * Is element a td?
     */
    private static boolean isTDorTH(Element element) {
        String tagName = DOM.getElementProperty(element, "tagName");
        return tagName.equalsIgnoreCase("td") || tagName.equalsIgnoreCase("th");
    }

    /**
     * Get the row for the element (a td or tr from this table) or -1 if
     * none.
     */
    public int getElementRow(Element element) {
        if (isTDorTH(element)) {
            element = DOM.getParent(element);
        }
        return DOM.getChildIndex(bodyElem, element);
    }

    /**
     * Get the column for the td or -1 if none.
     */
    public int getElementCol(Element td) {
        return DOM.getChildIndex(DOM.getParent(td), td);
    }

    /**
     * Get the TR element at row.
     */
    public Element getTR(int row) {
        return DOM.getChild(bodyElem, row);
    }

    /**
     * Get the TD element at row, col.
     */
    public Element getTD(int row, int col) {
        return DOM.getChild(getTR(row), col);
    }

    /**
     * Get the inner text of the TD at row, col.
     */
    public String getText(int row, int col) {
        return DOM.getInnerText(getTD(row, col));
    }

    /**
     * Scroll so the row is visible. NOP if row is out of range or we are
     * not attached.
     */
    public void scrollIntoView(int row) {
        if (row > 0 && row < data.size() && isAttached()) {
            DOM.scrollIntoView(getTR(row));
        }
    }

    public int getSelectedIndex() {
        return selectedIndex;
    }

    public void setSelectedIndex(int selectedIndex) {
        setSelectedIndex(selectedIndex, -1);
    }

    public void setSelectedIndex(int selectedIndex, int col) {
        if (this.selectedIndex != selectedIndex) {
            if (this.selectedIndex >= 0) {
                setSelectedStyle(dataToRow.get(this.selectedIndex), false);
            }
            this.selectedIndex = selectedIndex;
            if (showSelection && selectedIndex > 0) {
                int row = dataToRow.get(this.selectedIndex);
                setSelectedStyle(row, true);
                scrollIntoView(row);
            }
        }
        setSelectedColumn(col);
        ChangeEvent.fireNativeEvent(Document.get().createChangeEvent(), this);
    }

    /**
     * Get the selected object or null if none.
     */
    public Object getSelectedObject() {
        if (selectedIndex > 0 && selectedIndex < data.size()) {
            return data.get(selectedIndex);
        }
        return null;
    }

    /**
     * Set the selected object. Clears selection if not found.
     */
    public void setSelectedObject(Object o) {
        for (int i = 1; i < data.size(); i++) {
            if (data.get(i) == o) {
                setSelectedIndex(i, -1);
                return;
            }
        }
        setSelectedIndex(-1, -1);
    }

    /**
     * Find the TR for row and add/remove {@link #getSelectedStyleName()}.
     * NOP if row is out of range.
     */
    protected void setSelectedStyle(int row, boolean on) {
        if (row > 0 && row < data.size()) {
            Element tr = getTR(row);
            setStyleName(tr, selectedStyleName, on);
        }
    }

    public boolean isShowSelection() {
        return showSelection;
    }

    public void setShowSelection(boolean showSelection) {
        this.showSelection = showSelection;
    }

    /**
     * Get the selected column on the grid. This is only valid if there is
     * a selected index and it was selected using a mouse click.
     */
    public int getSelectedColumn() {
        return selectedColumn;
    }

    public void setSelectedColumn(int selectedColumn) {
        this.selectedColumn = selectedColumn;
    }

    public HandlerRegistration addClickHandler(ClickHandler clickHandler) {
        return addDomHandler(clickHandler, ClickEvent.getType());
    }

    public HandlerRegistration addMouseDownHandler(MouseDownHandler mouseDownHandler) {
        return addDomHandler(mouseDownHandler, MouseDownEvent.getType());
    }

    public HandlerRegistration addMouseUpHandler(MouseUpHandler mouseUpHandler) {
        return addDomHandler(mouseUpHandler, MouseUpEvent.getType());
    }

    public HandlerRegistration addMouseOutHandler(MouseOutHandler mouseOutHandler) {
        return addDomHandler(mouseOutHandler, MouseOutEvent.getType());
    }

    public HandlerRegistration addMouseOverHandler(MouseOverHandler mouseOverHandler) {
        return addDomHandler(mouseOverHandler, MouseOverEvent.getType());
    }

    public HandlerRegistration addMouseMoveHandler(MouseMoveHandler mouseMoveHandler) {
        return addDomHandler(mouseMoveHandler, MouseMoveEvent.getType());
    }

    public HandlerRegistration addMouseWheelHandler(MouseWheelHandler mouseWheelHandler) {
        return addDomHandler(mouseWheelHandler, MouseWheelEvent.getType());
    }

    public HandlerRegistration addChangeHandler(ChangeHandler changeHandler) {
        return addDomHandler(changeHandler, ChangeEvent.getType());
    }

    public int compare(Integer i1, Integer i2) {
        T o1 = data.get(i1);
        T o2 = data.get(i2);
        if (o1 == null && o2 == null) {
            return 0;
        }
        if (o1 == null) {
            return -1;
        }
        if (o2 == null) {
            return 1;
        }
        int value = renderer.compare(this, o1, o2, sortCol);
//        int index = 0;
//        while (value == 0)
        //TODO Use sort state
        if (sortDesc) {
            return -value;
        } else {
            return value;
        }
    }

    public abstract static class Renderer<T> {

        protected String[] headers;
        protected Column[] columns;

        protected Renderer(Column[] columns) {
            this.columns = columns;
        }

        public abstract Object getCell(T obj, int col);

        public CellRenderer getCellRenderer(int col) {
            return columns[col].renderer;
        }

        public int compare(EasyGrid grid, T o1, T o2, int sortCol) {
            Object c1 = getCell(o1, sortCol);
            Object c2 = getCell(o2, sortCol);
            Column col = columns[sortCol];
            CellComparator comparator = col.comparator;
            return comparator.compare(c1, c2);
            
        }

        public void renderHeader(EasyGrid grid, StringBuffer html) {
            int sortIndex = grid.sortCol;
            html.append("<tr class=\"");
            html.append(grid.headerStyleName);
            html.append("\">");
            for (int i = 0; i < columns.length; i++) {
                Column col = columns[i];
                if (col.attributes == null) {
                    html.append("<th>");
                } else {
                    html.append("<th ");
                    html.append(col.attributes);
                    html.append('>');
                }
                html.append(col.title == null ? "null" : col.title);
                if (col.isSortable()) {
                    html.append("&nbsp;<img border=\"0\" width=\"9\" height=\"8\" src=\"");
                    if (i == sortIndex) {
                        if (grid.sortDesc) {
                            html.append(grid.sortDescImage);
                        } else {
                            html.append(grid.sortAscImage);
                        }
                    } else {
                        html.append("clear.cache.gif");
                    }
                    html.append("\"/>");
                }
                html.append("</td>");
            }
            html.append("</tr>");
        }

        public void renderRow(EasyGrid grid, StringBuffer html, int row, T obj) {
            if (obj == null) {
                return;
            }
            html.append("<tr>");
            for (int i = 0; i < columns.length; i++) {
                Column col = columns[i];
                Object cell = getCell(obj, i);
                CellRenderer renderer = col.renderer;
                if (renderer == null) {
                    renderer = CellRenderer.CELL_RENDERER_STRING;
                }
                renderer.renderCell(cell, html);
            }
            html.append("</tr>");
        }

        public boolean isVisible(EasyGrid grid, T obj) {
            for (int i = 0; i < columns.length; i++) {
                Column col = columns[i];
                Object cell = getCell(obj, i);
                if (col.filter != null && !col.filter.isVisible(cell)) {
                    return false;
                }
            }
            return true;
        }

    }

    public static class Column<CT> {
        public String title;
        public String attributes;
        public CellRenderer<CT> renderer;
        public CellComparator<CT> comparator;
        public CellFilter<CT> filter;

        public Column(String title, String attributes, CellRenderer<CT> renderer, CellComparator<CT> comparator) {
            this.title = title;
            this.attributes = attributes;
            this.renderer = renderer;
            this.comparator = comparator;
        }

        public Column(String title, String attributes, Class<CT> autoAssignClass) {
            this(title, attributes, null, null);
            if (autoAssignClass != null) {
                renderer = CellRenderer.getRendererByClass(autoAssignClass);
                comparator = CellComparator.getComparatorByClass(autoAssignClass);
            }
        }

        public Column(String title) {
            this(title, null, null, null);
        }

        public Column(String title, String attributes) {
            this(title, attributes, null, null);
        }

        public Column(String title, Class<CT> autoAssignClass) {
            this(title, null, autoAssignClass);
        }

        public Column(String title, String attributes, CellRenderer<CT> renderer) {
            this(title, attributes, renderer, null);
        }

        public Column(String title, String attributes, CellComparator<CT> comparator) {
            this(title, attributes, null, comparator);
        }

        public boolean isSortable() {
            return comparator != null;
        }
    }

    public abstract static class CellRenderer<CT> {
        public static final CellRenderer CELL_RENDERER_STRING = new CellRendererString();
        public static final CellRenderer CELL_RENDERER_BOOLEAN = new CellRendererBoolean();
        public static final CellRenderer CELL_RENDERER_SHORT_DATE = new CellRendererShortDate();
        public static final CellRenderer CELL_RENDERER_FULL_DATE = new CellRendererFullDate();

        public static final String CELL_NONE = "<td>" + ClientUtil.NONE + "</td>";

        public abstract void renderCell(CT obj, StringBuffer html);

        public static CellRenderer getRendererByClass(Class cls) {
            if (cls.equals(Date.class)) {
                return CELL_RENDERER_SHORT_DATE;
            } else if (cls.equals(Boolean.class)) {
                return CELL_RENDERER_BOOLEAN;
            } else {
                return CELL_RENDERER_STRING;
            }
        }

    }

    private static class CellRendererString extends CellRenderer<Object> {
        public void renderCell(Object obj, StringBuffer html) {
            if (obj == null) {
                html.append(CELL_NONE);
            } else {
                html.append("<td>").append(ClientUtil.noneIfNull(obj.toString())).append("</td>");
            }
        }
    }

    private static class CellRendererBoolean extends CellRenderer<Boolean> {
        public void renderCell(Boolean obj, StringBuffer html) {
            if (obj == null) {
                html.append(CELL_NONE);
            } else {
                html.append("<td>").append(obj ? ClientUtil.YES : ClientUtil.NO).append("</td>");
            }
        }
    }

    private static class CellRendererFullDate extends CellRenderer<Date> {
        public void renderCell(Date obj, StringBuffer html) {
            if (obj == null) {
                html.append(CELL_NONE);
            } else {
                html.append("<td>").append(DateUtil.format(obj, true, true, true, true)).append("</td>");
            }
        }
    }

    private static class CellRendererShortDate extends CellRenderer<Date> {
        public void renderCell(Date obj, StringBuffer html) {
            if (obj == null) {
                html.append(CELL_NONE);
            } else {
                html.append("<td>").append(DateUtil.format(obj, true, true, false, false)).append("</td>");
            }
        }
    }

    public abstract static class CellComparator<CT>  {
        public static final CellComparator CELL_COMPARATOR_COMPARABLE = new CellComparatorComparable();
//        public static final CellComparator CELL_COMPARATOR_STRING = new CellComparatorString();
        public static final CellComparator CELL_COMPARATOR_STRING_IGNORE_CASE = new CellComparatorStringIgnoreCase();

        public abstract int compare(CT o1, CT o2);

        public static CellComparator getComparatorByClass(Class cls) {
            if (cls.equals(String.class)) {
                return CELL_COMPARATOR_STRING_IGNORE_CASE;
            } else {
                return CELL_COMPARATOR_COMPARABLE;
            }
        }
    }

//    private static class CellComparatorString extends CellComparator<String> {
//        public int compare(String o1, String o2) {
//            return o1.compareTo(o2);
//        }
//    }

    private static class CellComparatorStringIgnoreCase extends CellComparator<String> {
        public int compare(String o1, String o2) {
            return o1.compareToIgnoreCase(o2);
        }
    }

//    private static class CellComparatorDate extends CellComparator<Date> {
//        public int compare(Date o1, Date o2) {
//            return o1.compareTo(o2);
//        }
//    }

    private static class CellComparatorComparable extends CellComparator<Comparable> {
        public int compare(Comparable o1, Comparable o2) {
            return o1.compareTo(o2);
        }
    }

    public static abstract class CellFilter <CT> {
        public abstract boolean isVisible(CT obj);
    }

    /**
     * Remembers how the grid is sorted.
     */
    public static class SortState {

        private ArrayList cols = new ArrayList();

        public void add(int col, boolean desc) {
            if (cols.size() >= 8) {
                cols.remove(0);
            }
            cols.add(new Object[]{
                    new Integer(col),
                    desc ? Boolean.TRUE : Boolean.FALSE});
        }

        public void restore(EasyGrid fg) {
            for (int i = 0; i < cols.size(); i++) {
                Object[] a = (Object[]) cols.get(i);
                int col = ((Integer) a[0]).intValue();
                boolean desc = ((Boolean) a[1]).booleanValue();
                fg.sort(col, desc);
            }
        }
    }

    public static class Cell {
        public int row;
        public int col;

        public Cell() {
        }

        public Cell(int row, int col) {
            this.row = row;
            this.col = col;
        }
    }
}
